Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic property package: Using constraints to define state variables #1554

Closed
wants to merge 3 commits into from

Conversation

alma-walmsley
Copy link

@alma-walmsley alma-walmsley commented Dec 22, 2024

Fixes # .

Summary/Motivation:

As part of Ahuora we are using the generic property package framework to build custom property packages.

The code looks something like this:

m.fs.properties = GenericParameterBlock(**configuration)  # FTPx
m.fs.state = m.fs.properties.build_state_block([0], defined_state=True)
sb = m.fs.state[0]
sb.flow_mol.fix(1)  # mol/s
sb.temperature.fix(300)  # K
sb.pressure.fix(100000)  # Pa
sb.mole_frac_comp["benzene"].fix(0.5)
sb.mole_frac_comp["toluene"].fix(0.5)

m.fs.state.initialize()

Suppose that we know the value for flow_mass and want to use that instead. We can do this using a Constraint:

sb.flow_mol.unfix()
sb.flow_mass_constraint = Constraint(rule=sb.flow_mass == 1)  # kg/s

However, this ends up running into issues during initialization:

  • The initialization routine starts by fixing all state variables that are not currently fixed. This means flow_mol gets fixed anyway, and the block becomes over-defined. However, we are able to prevent this by setting the kwarg state_vars_fixed=True, which means initialization will skip trying to fix the state variables (and just check for 0 degrees of freedom). So this is not an issue at this stage.
  • During the initialization routine, constraints are deactivated at the start and then re-activated during the different initialization steps. The constraint flow_mass_constraint gets deactivated during the "Bubble, dew, and critical point initialization" step (which is fine), but needs to be activated for the "Phase equilibrium initialization" step, to ensure 0 degrees of freedom.

The specific error I get is

idaes.core.util.exceptions.InitializationError: fs.state Unexpected degrees of freedom during initialization at phase equilibrium step: 1.

Changes proposed in this PR:

  • This PR proposes an approach to activate the flow_mass_constraint (or other constraints as needed) before the "Phase equilibrium initialization" step. This is done by "tagging" the constraint with defining_state_var = True to indicate that this constraint defines a state variable, and should be activated.

For example,

sb.flow_mol.unfix()
sb.flow_mass_constraint = Constraint(rule=sb.flow_mass == 1)  # kg/s
sb.flow_mass_constraint.defining_state_var = True
m.fs.state.initialize(state_vars_fixed=True)

I added this check to both the _GenericStateBlock and ModularPropertiesInitializer initialize methods. I am not too sure how the ModularPropertiesInitializer class is used (seems to be the same logic) but I assume the code fits in both.

Comments on this approach would be useful.

Legal Acknowledgement

By contributing to this software project, I agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the license terms described in the LICENSE.txt file at the top level of this directory.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

@alma-walmsley
Copy link
Author

I'm having degree-of-freedom issues with unfixing pressure (for example, using enth_mol and temperature to define pressure) in the "Bubble, dew, and critical point initialization." So this may require some more thought - I'll probably have a closer look into how vars and constraints influence the degrees of freedom.

@andrewlee94
Copy link
Member

@alma-walmsley My first reaction to this is that you are opening a huge can of worms with potential edge cases - what happens if someone else had a different know input and constraint?

The way I would suggest doing this (and the way the framework was generally built) is that the constraint on flow_mass should live on the flowsheet level, and that when doing the initialization you use your best guess for flow_mol. Then, once the unit/state has been initialized, when you solve the flowsheet it will hopefully adjust the molar flowrate such that the mass flowrate constraint is satisfied.

@alma-walmsley
Copy link
Author

It is interesting that you suggest applying constraints at the flowsheet level. This is what we were initially doing, but since adding constraints directly to the state block instead, we have been able to solve a lot more consistently. If constraints can be applied at the state block level, this gives us "perfect" initialization and means the flowsheet can be solved in very few iterations. I think this is better because it breaks the problem into smaller subproblems - rather than adding all the complexity at the flowsheet level, which heavily relies on good guesses for the state variables. Relying on user guesses for state variables such as enthalpy (which the user may not know), or using the defaults from the property package, can cause both problems in initialization as well as set the flowsheet solve up to fail when applying constraints.

I am aware that this has probably not been the approach when developing unit models. For example, the heat exchanger initialization routine fixes temperature during one of its initialization steps, and this can mean too few degrees of freedom if we want to unfix temperature and use a constraint instead. At the moment, we're overriding the heat exchanger unit model to fix this for our implementation.

I tend to agree that the code in this PR isn't the best, and feels like a "hack" more than anything. But I think that long term, the state block constraints approach is the best solution to ensure consistent solving without relying too much on user-guesses.

We do this with a custom state block (inheriting from GenericStateBlock, HelmholtzStateBlock etc), which manages the constraints and how state variables are fixed during before initialization for 0 degrees of freedom. I think we would be happy to contribute some of this code in our property packages repository towards the main IDAES repo. This would mean the state block constraints approach becomes a feature and we would help to maintain it.

@andrewlee94
Copy link
Member

andrewlee94 commented Jan 16, 2025

So, the underlying issue here is that the initialization routines were built on the assumption that the model would be square if/when all the input conditions were fixed. Thus, the routines work by fixing all the inlet states and the working through the problem. By adding additional constraints to the unit model however, this breaks that assumptions and thus all the initialization routines would fail.

When you say "but since adding constraints directly to the state block instead, we have been able to solve a lot more consistently", what were the issues you were seeing previously? This sounds like it would be a case of your initial guess was not very good - for the case of fixing flow_mass instead of flow_mol, that sounds like you need a flowsheet level mini-routine that would handle this conversion. Additionally, if you are not doing it already, I would:

  1. Solve the unit with my best-guess flow_mol
  2. At the flowsheet level, solve the unit model plus the additional constraint. I.e., get the unit model right before connecting it to the rest of the flowsheet.
  3. Only then move on to the next unit model.

In fact, I just thought of what might be the correct solution: these additional constraints should be done as a plug-in to the unit model (similar to how costing is (should?) be implemented). Basically, a plug-in is a sub-model that can be attached to a unit model which comes with its own sub-routine for initialization. The new General Hierarchical Initializer already contains code that should handle the plug-in automatically (it will initialize the main unit model, and then call the plug-in initialization - note this is not heavily tested so might have some bugs).

@ksbeattie ksbeattie added the Priority:Normal Normal Priority Issue or PR label Jan 16, 2025
@alma-walmsley
Copy link
Author

alma-walmsley commented Jan 17, 2025

So, the underlying issue here is that the initialization routines were built on the assumption that the model would be square if/when all the input conditions were fixed. Thus, the routines work by fixing all the inlet states and the working through the problem. By adding additional constraints to the unit model however, this breaks that assumptions and thus all the initialization routines would fail.

This sounds like it would be a case of your initial guess was not very good

Correct, and the approach we are using is an attempt to manage both of these issues.

The unit model starts by initializing the control volume which first initializes the inlet state. If we follow the traditional approach of requiring all the state variables to be fixed:

  • Fixing the state variables to the default values provided in the property package is not very reliable,
  • Therefore, we need good guesses for the state variables to successfully initialize the inlet state block (let alone the unit model).
  • The user may not have a good guess for all the state variables, and/or requiring the user to input guesses all the time is tedious.

Instead of fixing all the inlet state vars, equality constraint(s) are added to the inlet instead. We still have a square model because each added constraint is accompanied by unfixing one of the state variables. For example, we could fix temperature, pressure, but not flow_mol, while adding an equality constraint for flow_mass - meaning we will still have 0 degrees of freedom. This is a shift in thinking from the traditional approach of "all state variables should be fixed".

Applying constraints at the state block level:

  • We don't necessarily need to rely on good default values or user guesses to successfully initialize the state block (though these will improve it)
  • State block initialization is "perfect" (ie. using a constraint for flow_mass will solve flow_mol to the exact value)
  • It is easier for the solver to solve the smaller problem (ie. a single state block) rather than the entire unit model.
  • This is at the expense of having to maintain code to allow constraints in the state block initialization - this PR

I am aware that some of the unit model initialization routines will not like the use of constraints (I mentioned heat exchanger above). I think we can get around this problem by:

  1. Deactivate state block constraints, and fix all the state variables
  2. Call initialize for the unit model
  3. When state block initialization is called, activate the state block constraints, initialize the state block, and then deactivate the constraints (have to keep track of which state vars are "guesses")
  4. When unit model initialization completes, re-activate the state block constraints, and unfix the necessary state vars.

Since we're managing unit model initialization (within sequential decomposition) and state block initialization (with our extended state block) we can handle this all on our end. It just requires some support for initializing a state block with constraints instead of state vars.

@andrewlee94
Copy link
Member

This is the key point here:

This is a shift in thinking from the traditional approach of "all state variables should be fixed".

This is a valid approach, but you are basically invaliding the core assumption that all initialisation was built on, thus if you want this to be a general implementation you are effectively looking to rebuild the entire initialization infrastructure. Speaking from experience, that is a lot of work to implement, and even more to test, so just make sure you are aware of what you are getting yourself into if you go down this route.

However, as I mentioned in another comment somewhere - take a look at the hierarchical Initializers and their support for plug-ins. This may do exactly what you are thinking of already (or at least something close enough). Alternatively, because the new Initializers are separate classes, it means you can write your own custom initialization routines for these specific cases (which I assume are only for inlets) and thus can use the existing initialization elsewhere.

@alma-walmsley
Copy link
Author

@bertkdowns thoughts?

@alma-walmsley
Copy link
Author

Based on our discussion today I am closing this PR, and we will have to come up with another solution to resolve this sort of problem. Thanks for everyone's input!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Priority:Normal Normal Priority Issue or PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants